package com.rao.service.impl;

import com.rao.config.websecurity.core.SecurityContextUtil;
import com.rao.entity.CourseSelection;
import com.rao.entity.VO.CommonResponseVO;
import com.rao.entity.bean.CUBean;
import com.rao.mapper.CourseSelectionMapper;
import com.rao.mideng.annotation.RaoLogAnno;
import com.rao.mideng.annotation.RaoMiDeng;
import com.rao.service.ICourseSelectionService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 选课表 服务实现类
 * </p>
 *
 * @author rao3030
 * @since 2021-11-04
 */
@Service
@Slf4j
public class CourseSelectionServiceImpl extends ServiceImpl<CourseSelectionMapper, CourseSelection>
        implements ICourseSelectionService, InitializingBean {

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    RedisTemplate redisTemplate;
    private final String stock_prefix_redis_key = "stock_";
    private static final String redisTag = "handler-res";

    @Value("${rao.segment-count:4}")
    private int segmentCount;

    private ConcurrentHashMap<String, ConcurrentHashMap<String, Boolean>> segmentMapFlag;

    @Autowired
    private RedissonClient redissonClient;

    //要做到动态改变的话，这个还是要放到一个配置bean里面，不要放在KillGoodsServiceImpl下面，依赖注入这个bean就好了，引用传递
    //就需要借助配置中心+@RefreshScope。调用一下refresh的url
    //配置成数组，不要使用list
    @Value("${rao.good-ids:1001:100,1002:200,1003:100}")
    public String[] goodIds;


    public CourseSelection courseSelectionById(Integer killId) {//dcl
        Object o1 = redisTemplate.opsForValue().get("cache" + killId);
        if (o1 == "miss") {
            return null;
        }
        if (null == o1) {
            Object o2 = null;
            RLock lock = redissonClient.getLock("cache" + killId + "lock");
            lock.lock(5, TimeUnit.SECONDS);
            try {
                o2 = redisTemplate.opsForValue().get("cache" + killId);
                if (o2 == "miss") {
                    return null;
                }
                if (o2 == null) {
                    CourseSelection byId = this.getById(killId);
                    if (byId == null) {
                        redisTemplate.opsForValue().set("cache" + killId, "miss", 30, TimeUnit.SECONDS);
                        return null;
                    } else {
                        redisTemplate.opsForValue().set("cache" + killId, byId, 30, TimeUnit.MINUTES);
                    }
                    log.info("查询数据库");
                    return byId;
                }
            } catch (Exception exception) {
                exception.printStackTrace();
            } finally {
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
            log.info("缓存命中");
            return (CourseSelection) o2;
        }
        log.info("缓存命中");
        return (CourseSelection) o1;
    }

    private Queue<CUBean> queue = new ConcurrentLinkedQueue<>();

    @RaoLogAnno
    @Override
    public boolean grepLessByRedisSegment(Integer killId) {
        //禁止重复选该课
        if (stringRedisTemplate.opsForValue().setBit(redisTag + "kill" + killId, SecurityContextUtil.getUserId(), true)) {
            return false;
        }
        String javaMapKey = stock_prefix_redis_key + killId;
        if (isStockEmpty(javaMapKey)) {
            log.info("=======无课=======");
            return false;
        }
        List<String> stockKeys = getStockKeyByKillId(javaMapKey);
        String redisKey = stockKeys.get(new Random().nextInt(stockKeys.size()));
        RLock lock = redissonClient.getLock(redisKey + "lock");

        /*这里可以加段query db ，但是 会出现问题 redis突然lru掉了，数据就乱了*/

        lock.lock(5, TimeUnit.SECONDS);//这种方式是没有看门狗的
        try {
            //查询缓存
            String stockStr = stringRedisTemplate.opsForValue().get(redisKey);
            int stockNum = Integer.parseInt(stockStr);
            if (stockNum <= 0) {
                //设置没有了库存
                segmentMapFlag.get(javaMapKey).put(redisKey, Boolean.FALSE);
                //走其他
                return grepLessByRedisSegment(killId);
            } else {
                Long decrement = stringRedisTemplate.opsForValue().decrement(redisKey);
                if (decrement >= 0) {
                    log.info("抢到了----" + redisKey + "====" + decrement);
                    //成功就加入到有user-id标识的地方 bitmap
                    stringRedisTemplate.opsForSet().add(redisTag + killId, String.valueOf(SecurityContextUtil.getUserId()));
                    return true;
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage());
            throw e;
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        return false;
    }


    @Autowired
    JdbcTemplate jdbcTemplate;

    /*
    UPDATE `student-manager`.`tb_split_stock1` SET `cou_sel_id` = 1111, `stock` = '100' WHERE `id` = 1;
    UPDATE `student-manager`.`tb_split_stock2` SET `cou_sel_id` = 1111, `stock` = '100' WHERE `id` = 1;
    UPDATE `student-manager`.`tb_split_stock3` SET `cou_sel_id` = 1111, `stock` = '100' WHERE `id` = 1;
    UPDATE `student-manager`.`tb_split_stock4` SET `cou_sel_id` = 1111, `stock` = '100' WHERE `id` = 1;

     */
    @Transactional//加了事务注解之后，一旦出现请求数>库存数。就会出现死锁，不加事务注解就不会出现死锁
    @Override
    public int grepLessByDB(Integer killId) {
        //内部使用了cas
        int idx = random.nextInt(4) + 1;//[1-4] 第一次走随机
        String tableName = "tb_split_stock" + idx;
        int update = jdbcTemplate.update("update " + tableName + " set stock= stock-1 where stock>0 and cou_sel_id=?", killId);
        if (update == 1) {
            log.info("第一次随机挑选库存成功,坐标-{}", idx);
            return 1;
        }
        return grepLess(killId, (idx + 1) % 4 + 1, "tb_split_stock", 0);//[1-4]
    }

    private static final Random random = new Random();

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int grepLess(Integer killId, int idx, String tableName, int count) {
        if (count >= 3) {
            log.info("没有拿到库存");
            return 0;
        }
        int update = jdbcTemplate.update("update " + (tableName + idx) + " set stock= stock-1 where stock>0 and cou_sel_id=?", killId);
        if (update == 1) {
            log.info("第一次随机失败，之后当前线程又总共试了{}次-拿到了库存，坐标-{}", count + 1, idx);
            return 1;
        }
        return grepLess(killId, (idx + 1) % 4 + 1, tableName, count + 1);
    }


    @Override
    @Transactional
    @RaoLogAnno
    @RaoMiDeng
    public boolean ackTheLess(Integer killId) {
        if (stringRedisTemplate.opsForValue().getBit(redisTag + "kill" + killId, SecurityContextUtil.getUserId())) {
            int update = jdbcTemplate.update("update tb_course_selection set sum =sum -1 where sum>=0 and id=?", killId);
            //之后不允许重复选该课程
            stringRedisTemplate.opsForValue().setBit(redisTag + "kill" + killId, SecurityContextUtil.getUserId(), true);
            return update == 1;
        }
        return true;
    }

    @Override
    public boolean dropTheLess(Integer killId) {
        return false;
    }


    private boolean isStockEmpty(String killID) {
        for (Map.Entry<String, Boolean> entry : segmentMapFlag.get(killID).entrySet()) {
            if (entry.getValue()) {
                return false;
            }
        }
        return true;
    }

    private List<String> getStockKeyByKillId(String killId) {
        ConcurrentHashMap<String, Boolean> map = segmentMapFlag.get(killId);
        List<String> keys = new ArrayList<>(4);
        for (Map.Entry<String, Boolean> entry : map.entrySet()) {
            if (entry.getValue()) {
                keys.add(entry.getKey());
            }
        }
        return keys;
    }

    /**
     * rao.good-ids: 1111:100 , 2222:200 , 3333:50 , 4444:200
     * <p>
     * 1) "stock_22221"
     * 2) "stock_44442"
     * 3) "stock_11114"
     * 4) "stock_33332"
     * 5) "stock_22223"
     * 6) "stock_33333"
     * <p>
     * 缓存预热的工作
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        segmentMapFlag = new ConcurrentHashMap<>();
        for (String goodId : goodIds) {
            String[] split = goodId.split(":");
            //商品id--killID1001
            String goodsID = split[0].trim();
            //每个段的库存数--100
            String goodsStock = split[1].trim();
            ConcurrentHashMap<String, Boolean> map = new ConcurrentHashMap<>();
            for (int i = 1; i <= segmentCount; i++) {
                //stock_10001_1 stock_10001_2 stock_10001_3...
                String stockKey = stock_prefix_redis_key + goodsID + i;
                //缓存预热
                stringRedisTemplate.opsForValue().setIfAbsent(stockKey, goodsStock, 30, TimeUnit.MINUTES);
                map.put(stockKey, Boolean.TRUE);
            }
            segmentMapFlag.put(stock_prefix_redis_key + goodsID, map);
        }
    }
}
